<?php

declare(strict_types=1);

namespace WebTus;

use Exception;

defined('UPLOAD_POST_NAME') || define('UPLOAD_POST_NAME', 'post_name'); //原生上传文件表单名
defined('UPLOAD_FILE_NAME') || define('UPLOAD_FILE_NAME', 'filename'); //原生上传文件名
defined('UPLOAD_PATH_NAME') || define('UPLOAD_PATH_NAME', 'path'); //原生上传文件路径

class TusService
{
    /**
     * 1, 文件名+文件大小+修改日期 为key
     * 2, 文件sha1 为key
     */

    public const STATUS_CODE_100 = 100;

    public const STATUS_CODE_200 = 200;
    public const STATUS_CODE_201 = 201;
    public const STATUS_CODE_204 = 204;
    public const STATUS_CODE_206 = 206;

    public const STATUS_CODE_400 = 400;
    public const STATUS_CODE_403 = 403;
    public const STATUS_CODE_404 = 404;
    public const STATUS_CODE_405 = 405;
    public const STATUS_CODE_409 = 409;
    public const STATUS_CODE_410 = 410;
    public const STATUS_CODE_412 = 412;
    public const STATUS_CODE_413 = 413;
    public const STATUS_CODE_415 = 415;
    public const STATUS_CODE_416 = 416;
    public const STATUS_CODE_423 = 423; // Locked (WebDAV) (RFC 4918)
    public const STATUS_CODE_460 = 460;

    public const STATUS_CODE_500 = 500;

    public static $httpCodeStatus = [
        // INFORMATIONAL CODES
        self::STATUS_CODE_100 => 'Continue',
        // SUCCESS CODES
        self::STATUS_CODE_200 => 'OK',
        self::STATUS_CODE_201 => 'Created',
        self::STATUS_CODE_204 => 'No Content',
        self::STATUS_CODE_206 => 'Partial Content',
        // CLIENT ERROR
        self::STATUS_CODE_400 => 'Bad Request',
        self::STATUS_CODE_403 => 'Forbidden',
        self::STATUS_CODE_404 => 'Not Found',
        self::STATUS_CODE_405 => 'Method Not Allowed',
        self::STATUS_CODE_409 => 'Conflict',
        self::STATUS_CODE_410 => 'Gone',
        self::STATUS_CODE_412 => 'Precondition Failed',
        self::STATUS_CODE_413 => 'Request Entity Too Large',
        self::STATUS_CODE_415 => 'Unsupported Media Type',
        self::STATUS_CODE_416 => 'Range Not Satisfiable',
        self::STATUS_CODE_423 => 'Locked',
        self::STATUS_CODE_460 => 'Checksum Mismatch',
        // SERVER ERROR
        self::STATUS_CODE_500 => 'Internal Server Error',
    ];

    //可浏览器渲染的MIME类型
    public static $mimeTypeInline = [
        "text/plain" => true,
        "image/png" => true,
        "image/jpeg" => true,
        "image/gif" => true,
        "image/bmp" => true,
        "image/webp" => true,

        "audio/mpeg" => true,
        "audio/ogg" => true,
        "audio/wav" => true,

        "audio/webm" => true,
        "video/webm" => true,

        "video/ogg " => true,
    ];

    protected $req_header = null;
    protected $res_header = [];
    protected $res_code = self::STATUS_CODE_200;
    protected $res_msg = '';
    /**
     * @var array|TusReadFile|null
     */
    protected $res_data = null;
    /**
     * @var TusStoreAbstract|TusStoreFile|TusStoreInterface
     */
    public $store;

    public $isSend = false;
    public $maxUploadSize = 0; //文件上传大小限制 0 无限制, 2147483648 2GB
    public $maxChunkSize = 0; //分块上传大小限制 0 无限制, 52428800 50M todo
    public $allowType = ''; //限制文件类型 如 jpg,png,bmp,rar,zip,txt
    public $notAllowType = ''; //不允许的文件类型 如 exe,php
    public $uploadExpires = 86400; //未完成上传文件过期时间

    public const PROTOCOL_VERSION = '1.0.0';

    public $allowMethods = 'GET,POST,PATCH,DELETE,HEAD,OPTIONS';
    public $allowAlgos = 'crc32,md5,sha1,sha256,sha512'; //签名算法 8 32 40 64 128
    public $extensions = 'creation,creation-with-upload,checksum,expiration,termination,concatenation,creation-defer-length';
    //指定文件标识用于断点续传
    public $uploadKeyName = 'Upload-Key'; //自定上传标识名 通过header[$uploadKeyName]或Upload-Metadata指定可自定的上传标识名
    public $enableChecksum = false; //启用POST创建时的checksum 校验
    public $outPathForHead = false; //允许head时输出文件路径
    /**
     * 是否支持使用 Upload-Metadata+Upload-Length 生成上传key
     * 必需 Upload-Metadata包含[filename|name], Upload-Length>0
     */
    /**
     * @var string|\Closure 可自定义日志记录
     */
    public static $logFile = '';
    public static $logSize = 4194304;
    public static $isLog = false;
    /**
     * 上传资源写入回调
     * @var null|\Closure ($info, $maxSize)
     */
    public $writeInStream = null;
    public $customInStream = ''; //指定自定上传资源路径

    public function __construct($store)
    {
        $this->store = $store;
        $this->store->tus = $this;
        if ($this->store->fileHashAlgorithm) { //hash去重
            $this->store->fileCoverWays = TusStoreAbstract::FILE_DUPLICATE; //hash时允许重复名
        }
        if (!static::$logFile) {
            static::$logFile = dirname(__DIR__) . '/../tus.log';
        }
    }

    public static function json($content)
    {
        return \json_encode($content, defined('JSON_UNESCAPED_UNICODE') ? JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES : 0);
    }

    public static function getMethod()
    {
        return isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']) ? strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']) : ($_SERVER['REQUEST_METHOD'] ?? 'GET');
    }

    public static function getSiteUrl()
    {
        $scheme = 'http';
        if (isset($_SERVER['REQUEST_SCHEME']) && $_SERVER['REQUEST_SCHEME'] == 'https') {
            $scheme = 'https';
        } elseif (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') {
            $scheme = 'https';
        }
        return $scheme . '://' . ($_SERVER['HTTP_X_FORWARDED_HOST'] ?? ($_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] . ($_SERVER['SERVER_PORT'] == '80' || $_SERVER['SERVER_PORT'] == '443' ? '' : ':' . $_SERVER['SERVER_PORT'])));
    }

    public static function getUri()
    {
        return $_SERVER['REQUEST_URI'] ?? ($_SERVER['PHP_SELF'] ?? ($_SERVER['SCRIPT_NAME'] ?? '')) . (empty($_SERVER['QUERY_STRING']) ? '' : '?' . $_SERVER['QUERY_STRING']);
    }

    public static function getPathInfo()
    {
        if (isset($_SERVER['REQUEST_URI'])) {
            $pos = strpos($_SERVER['REQUEST_URI'], '?');
            return $pos ? substr($_SERVER['REQUEST_URI'], 0, $pos) : $_SERVER['REQUEST_URI'];
        }
        return $_SERVER['PHP_SELF'] ?? ($_SERVER['SCRIPT_NAME'] ?? '');
    }

    // 简要的mime_type类型
    public static function minMimeType($filename)
    {
        static $mimeType = [
            'bmp' => 'image/bmp',
            'gif' => 'image/gif',
            'jpe' => 'image/jpeg',
            'jpeg' => 'image/jpeg',
            'jpg' => 'image/jpeg',
            'png' => 'image/png',
            'webp' => 'image/webp',
            'ico' => 'image/x-icon',

            'aac' => 'audio/aac',
            'amr' => 'audio/amr',
            'mp3' => 'audio/mpeg', //audio/mp3
            'wav' => 'audio/x-wav', //audio/wav
            'ogg' => 'audio/ogg',

            'avi' => 'video/x-msvideo',
            'mp4' => 'video/mp4',
            'mpeg' => 'video/mpeg',
            'ogv' => 'video/ogg',
            'webm' => 'video/webm',
            'flv' => 'video/x-flv',

            'txt' => 'text/plain',
            'csv' => 'text/csv',
            'xml' => 'application/xml',
            'pdf' => 'application/pdf',
            'doc' => 'application/msword',
            'ppt' => 'application/vnd.ms-powerpoint',
            'xls' => 'application/vnd.ms-excel',
            'xlt' => 'application/vnd.ms-excel',

            'tar' => 'application/x-tar',
            '7z' => 'application/x-7z-compressed',
            'rar' => 'application/x-rar-compressed',
            'zip' => 'application/zip',
        ];
        $mime = 'application/octet-stream';
        $pos = strrpos($filename, '.');
        if (!$pos) {
            return $mime;
        }
        if ($d = strpos($filename, '?')) {
            $filename = substr($filename, 0, $d);
        }
        $type = strtolower(substr($filename, $pos + 1));
        if (isset($mimeType[$type])) {
            $mime = $mimeType[$type];
        }
        return $mime;
    }

    //名称是否有效
    public static function isValidName($name)
    {
        $forbidden = ['\\', '/', ':', '*', '?', '"', '<', '>', '|'];
        foreach ($forbidden as $v) {
            if (strpos($name, $v) !== false) {
                return false;
            }
        }
        return true;
    }

    //仅记录指定大小的日志 超出大小重置重新记录
    public static function log($content)
    {
        if (is_file(static::$logFile) && static::$logSize <= filesize(static::$logFile)) {
            file_put_contents(static::$logFile, '', LOCK_EX);
            clearstatcache(true, static::$logFile);
        }
        if (func_num_args() > 1) {
            $args = func_get_args();
            $content = '';
            foreach ($args as $v) {
                $content .= (is_scalar($v) ? $v : self::json($v)) . ' ';
            }
        }
        if (static::$logFile instanceof \Closure) {
            call_user_func(static::$logFile, is_scalar($content) ? $content : self::json($content));
            return;
        }
        file_put_contents(static::$logFile, "[" . date("Y-m-d H:i:s") . "]" . (is_scalar($content) ? $content : self::json($content)) . "\n", FILE_APPEND);
    }

    public function getReqHeader($header_name = null, $default = null)
    {
        if ($this->req_header === null) {
            if (function_exists('getallheaders')) {
                $this->req_header = getallheaders();
            } else {
                foreach ($_SERVER as $name => $value) {
                    if (strncmp($name, 'HTTP_', 5) === 0) {
                        $_name = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))));
                        $this->req_header[$_name] = $value;
                    } elseif (strncmp($name, 'CONTENT_', 8) === 0) {
                        $_name = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 8)))));
                        $this->req_header[$_name] = $value;
                    }
                }
                if (!isset($this->req_header['Authorization'])) {
                    if (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
                        $this->req_header['Authorization'] = $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
                    } elseif (isset($_SERVER['PHP_AUTH_DIGEST'])) {
                        $this->req_header['Authorization'] = $_SERVER['PHP_AUTH_DIGEST'];
                    } elseif (isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) {
                        $this->req_header['Authorization'] = base64_encode($_SERVER['PHP_AUTH_USER'] . ':' . $_SERVER['PHP_AUTH_PW']);
                    }
                }
            }
        }

        if ($header_name === null) {
            return $this->req_header;
        }
        if (is_array($header_name)) {
            $values = [];
            foreach ($header_name as $item) {
                $values[$item] = $this->req_header[$item] ?? $default;
            }
            return $values;
        }
        return $this->req_header[$header_name] ?? $default;
    }

    public function getMetadata($name = 'ALL')
    {
        $metadataStr = $this->getReqHeader('Upload-Metadata');

        if (empty($metadataStr)) {
            return null;
        }

        $metadata = explode(',', $metadataStr);
        if ($name === 'ALL') {
            $result = [];
            foreach ($metadata as $value) {
                $item = explode(' ', $value);
                $result[$item[0]] = isset($item[1]) ? base64_decode($item[1]) : '';
            }
            return $result;
        }
        foreach ($metadata as $value) {
            $item = explode(' ', $value);

            if ($item[0] === $name) {
                return isset($item[1]) ? base64_decode($item[1]) : '';
            }
        }

        return '';
    }

    public function getKey()
    {
        $id = basename(self::getUri());
        if ($pos = strpos($id, '?')) {
            $id = substr($id, 0, $pos);
        }
        return $id;
    }

    public function getReqPath()
    {
        $uri = self::getPathInfo();
        if (strpos($uri, '../') !== false || strpos($uri, '..\\') !== false) {
            throw new \Exception('Unsupported', self::STATUS_CODE_500);
        }
        return urldecode($uri);
    }

    public function setResCode($code)
    {
        $this->res_code = $code;
        return $this;
    }

    public function setResHeader($name, $value = null)
    {
        if (is_array($name)) {
            $this->res_header = $value === true ? $name : array_merge($this->res_header, $name);
        } elseif ($name === null) {
            $this->res_header = [];
        } else {
            $this->res_header[$name] = $value;
        }
        return $this;
    }

    /**
     *
     * OPTIONS  /files              # 获取有关服务器目前配置的信息
     * HEAD     /files/{upload-key} # 检查上传的文件是否合法
     * POST     /files              # 创建
     * PATCH    /files/{upload-key} # 修改
     * DELETE   /files/{upload-key} # 删除
     *
     * @param $method
     * @return array|null
     */
    public function reqHandle($method = null): ?array
    {
        if ($method === null) {
            $method = ucfirst(strtolower(self::getMethod()));
        }
        if (!$this->middleware($method)) {
            return $this->response();
        }

        try {
            switch ($method) {
                case 'Options':
                    $this->handleOptions();
                    break;
                case 'Head':
                    $this->handleHead();
                    break;
                case 'Post':
                    $this->handlePost();
                    break;
                case 'Patch':
                    $this->handlePatch();
                    break;
                case 'Delete':
                    $this->handleDelete();
                    break;
                case 'Get':
                    $this->handleGet();
                    break;
                default:
                    $this->setResCode(self::STATUS_CODE_405);
                    break;
            }
        } catch (Exception $e) {
            $code = $e->getCode();
            $this->setResCode($code ?: self::STATUS_CODE_500);

            if ($code != self::STATUS_CODE_404) {
                static::log($e->getCode() . ':' . $e->getMessage());
                $this->res_msg = $e->getMessage();
                //static::log($e->getTraceAsString());
            }
        }
        return $this->response();
    }

    private function response(): ?array
    {
        if (!isset(self::$httpCodeStatus[$this->res_code])) {
            $this->res_code = self::STATUS_CODE_200;
        }

        static::$isLog && static::log('res', $this->res_code, self::$httpCodeStatus[$this->res_code], $this->res_header, PHP_EOL);

        if ($this->isSend === false) {
            return [$this->res_code, $this->res_header, $this->res_msg, $this->res_data];
        }

        if (headers_sent()) {
            return null;
        }

        $protocol = ($_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0');
        header($protocol . ' ' . $this->res_code . ' ' . self::$httpCodeStatus[$this->res_code]);
        foreach ($this->res_header as $name => $value) {
            header($name . ': ' . $value);
        }
        $output = $this->res_msg;
        if ($this->res_code >= self::STATUS_CODE_400) {
            if ($this->res_msg !== '') {
                $output = self::json(['code' => $this->res_code, 'msg' => $this->res_msg, 'data' => null]);
            }
        } else {
            if ($this->res_data instanceof TusReadFile) { //get 输出文件
                $fp = fopen($this->res_data->file, 'rb');
                $out = fopen('php://output', 'wb');
                stream_copy_to_stream($fp, $out, $this->res_data->size, $this->res_data->offset);
                fclose($fp);
                fclose($out);
                return null;
            } elseif (($this->res_code == self::STATUS_CODE_201 || $this->res_code == self::STATUS_CODE_200) && $this->res_data) {
                static::$isLog && static::log([$this->res_code, $this->res_header, $this->res_msg, $this->res_data]);
                $output = self::json(['code' => 0, 'msg' => 'ok', 'data' => $this->res_data]);
            }
        }
        if ($output !== '') {
            $out = fopen('php://output', 'w');
            fwrite($out, $output);
            fclose($out);
        }
        return null;
    }

    private function middleware($method)
    {
        //重置初始
        $this->req_header = null;
        $this->res_header = [
            'Tus-Resumable' => self::PROTOCOL_VERSION,
            'X-Content-Type-Options' => 'nosniff',
        ];
        $this->res_code = self::STATUS_CODE_200;
        $this->res_msg = '';
        $this->res_data = null;
        $this->customInStream = '';

        $clientVersion = $this->getReqHeader('Tus-Resumable');
        static::$isLog && static::log('req', $method . ' ' . static::getUri(), PHP_EOL, $this->req_header, PHP_EOL);

        if ($clientVersion && $method != 'Get' && $clientVersion != self::PROTOCOL_VERSION) {
            $this->setResCode(self::STATUS_CODE_412);
            return false;
        }

        if ($origin = $this->getReqHeader('Origin')) {
            $this->res_header['Access-Control-Allow-Origin'] = $origin;
            if ($method == 'Options') {
                $this->res_header['Access-Control-Allow-Methods'] = $this->allowMethods;
                $this->res_header['Access-Control-Allow-Headers'] = 'Origin, X-Requested-With, Content-Type, Upload-Length, Upload-Offset, Tus-Resumable, Upload-Defer-Length, Upload-Metadata, Upload-Concat, ' . $this->uploadKeyName;
                $this->res_header['Access-Control-Max-Age'] = 86400; //隔24小时才发起预检请求
            } else {
                $this->res_header['Access-Control-Expose-Headers'] = 'Upload-Length, Upload-Offset, Upload-Metadata, Tus-Version, Tus-Max-Size, Tus-Resumable, Tus-Extension, Location, Upload-Defer-Length, Upload-Concat, Upload-Expires, Tus-Checksum-Algorithm';
            }
        }

        return true;
    }

    /**
     * 获取有关服务器目前配置的信息
     * OPTIONS /files
     */
    private function handleOptions()
    {
        $headers = [
            'Tus-Version' => self::PROTOCOL_VERSION,
            'Tus-Extension' => $this->extensions,
            'Tus-Checksum-Algorithm' => $this->allowAlgos,
        ];

        if ($this->maxUploadSize > 0) {
            $headers['Tus-Max-Size'] = $this->maxUploadSize;
        }

        $this->setResCode(self::STATUS_CODE_204);
        $this->setResHeader($headers);
    }

    /**
     * 检查上传的文件是否合法
     * HEAD /files/{upload-key}
     */
    private function handleHead()
    {
        $id = $this->getKey();
        $info = $this->id2info($id, !empty($_GET['hash']));
        if (is_int($info)) {
            $this->setResCode($info);
            return;
        }

        if ($info->isTemp && !$this->store->lock($id)) {
            throw new Exception($id . '文件被锁定', self::STATUS_CODE_423);
        }

        $headers = [
            'Upload-Offset' => $info->offset,
            'Cache-Control' => 'no-store',
        ];
        if ($info->isPartial()) {
            $headers['Upload-Concat'] = 'partial';
        } elseif ($info->isFinal()) {
            $base = dirname(self::getUri());
            $headers['Upload-Concat'] = 'final;' . implode(' ', array_map(function ($key) use ($base) {
                return $base . '/' . $key;
            }, $info->partial));
        }
        if ($info->metadata) {
            if ($this->outPathForHead) { //允许输出文件路径
                $info->metadata['path'] = $info->path;
            }
            $metadataStr = '';
            foreach ($info->metadata as $k => $v) {
                $metadataStr .= ',' . $k . ' ' . base64_encode($v);
            }
            $headers['Upload-Metadata'] = substr($metadataStr, 1);
        }
        if ($info->sizeIsDeferred) {
            $headers['Upload-Defer-Length'] = 1;
        } else {
            $headers['Upload-Length'] = $info->size;
        }

        $info->isTemp && $this->store->unlock($id);
        $this->setResCode(self::STATUS_CODE_200);
        $this->setResHeader($headers);
    }

    /**
     * 普通模式单文件上传  name=表单文件名&path=指定路径&filename=指定文件名
     * @param $name
     * @throws Exception
     */
    private function filePost($name)
    {
        if (!isset($_FILES[$name])) {
            throw new Exception('未选择上传文件', self::STATUS_CODE_400);
        }
        //指定文件名 未指定使用上传的文件名
        $filename = '';
        if (isset($_POST[UPLOAD_FILE_NAME])) {
            $filename = trim($_POST[UPLOAD_FILE_NAME]);
        } elseif (isset($_GET[UPLOAD_FILE_NAME])) {
            $filename = trim($_GET[UPLOAD_FILE_NAME]);
        }
        //指定路径 未指定时使用自动目录
        $path = '';
        if (isset($_POST[UPLOAD_PATH_NAME])) {
            $path = trim($_POST[UPLOAD_PATH_NAME]);
        } elseif (isset($_GET[UPLOAD_PATH_NAME])) {
            $path = trim($_GET[UPLOAD_PATH_NAME]);
        }

        //模拟
        $this->customInStream = $_FILES[$name]['tmp_name']; //自定义上传数据
        $this->req_header['Content-Type'] = 'application/offset+octet-stream';
        $this->req_header['Upload-Length'] = $_FILES[$name]['size'];
        $this->req_header['Content-Length'] = $_FILES[$name]['size'];
        $metadataStr = '';
        $metadata = ['name' => $filename === '' ? $_FILES[$name]['name'] : $filename, 'path' => $path];
        if (isset($_FILES[$name]['type'])) {
            $metadata['type'] = $_FILES[$name]['type'];
        }
        foreach ($metadata as $k => $v) {
            $metadataStr .= ',' . $k . ' ' . base64_encode($v);
        }
        $this->req_header['Upload-Metadata'] = substr($metadataStr, 1);
    }

    /**
     * 创建上传
     * POST /files
     * @return void
     * @throws Exception
     */
    private function handlePost()
    {
        if (!empty($_GET[UPLOAD_POST_NAME])) { //有普通方式上传文件 模拟tus
            $this->filePost($_GET[UPLOAD_POST_NAME]);
        }

        $metadata = $this->getMetadata();
        static::$isLog && static::log('getMetadata', $metadata);

        $filename = '';
        if (isset($metadata['name'])) {
            $filename = $metadata['name'] = trim($metadata['name']);
        } elseif (isset($metadata['filename'])) {
            $filename = $metadata['name'] = trim($metadata['filename']);
            unset($metadata['filename']);
        }
        //文件名校验
        if ($filename === '' || !static::isValidName($filename)) {
            throw new Exception('文件名为空或无效', self::STATUS_CODE_400);
        }
        //类型限制
        if ($this->allowType !== '') {
            $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
            if (strpos(',' . $this->allowType . ',', ',' . $extension . ',') === false) {
                throw new Exception('仅支持上传' . $this->allowType . '的文件类型', self::STATUS_CODE_400);
            }
        } elseif ($this->notAllowType !== '') {
            $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
            if (strpos(',' . $this->notAllowType . ',', ',' . $extension . ',') !== false) {
                throw new Exception('不支持上传的文件类型', self::STATUS_CODE_400);
            }
        }

        //分块上传解析
        $containsChunk = $this->getReqHeader('Content-Type') === 'application/offset+octet-stream';
        $concat = $this->getReqHeader('Upload-Concat');
        $sizeIsDeferred = $this->getReqHeader('Upload-Defer-Length') === '1';
        $size = 0;
        $isFinal = $concat && strpos($concat, 'final;') === 0;
        $isPartial = $concat === 'partial';  //分块传输
        $partial = [];
        if ($isFinal) { //完成分块传输
            if ($containsChunk) {
                $this->setResCode(self::STATUS_CODE_403);
                return;
            }
            $list = explode(' ', substr($concat, 6));
            foreach ($list as $value) {
                $value = trim($value);
                if ($value === '') {
                    continue;
                }
                $key = basename($value);
                if ($key === '') {
                    continue;
                }
                $partInfo = $this->store->getInfo($key);

                if (!$partInfo) {
                    throw new Exception('Post -> Empty partInfo[' . $key . ']', self::STATUS_CODE_400);
                }
                if ($partInfo->sizeIsDeferred || $partInfo->offset != $partInfo->size) {
                    throw new Exception('Post -> ErrUploadNotFinished', self::STATUS_CODE_400);
                }

                $size += $partInfo->size;
                $partial[] = $key;
            }
            if (count($partial) === 0) {
                throw new Exception('Post -> ErrInvalidConcat', self::STATUS_CODE_400);
            }
        } else {
            $size = (int)$this->getReqHeader('Upload-Length');
            if ($size && $sizeIsDeferred) {
                throw new Exception('Post -> ErrInvalidUploadLength', self::STATUS_CODE_400);
            }
        }

        if ($this->maxUploadSize > 0 && $size > $this->maxUploadSize) {
            $this->setResCode(self::STATUS_CODE_413);
            return;
        }

        //取文件路径
        $path = $this->store->fmtPath($metadata['path'] ?? '');

        //取文件标识
        $id = trim($this->getReqHeader($this->uploadKeyName, ''));
        if ($id === '' && isset($metadata[$this->uploadKeyName])) {
            $id = trim($metadata[$this->uploadKeyName]);
        }
        unset($metadata[$this->uploadKeyName]);
        if ($id !== '') {
            if (strlen($id) < 3 || !static::isValidName($id)) {
                throw new Exception("Invalid upload-key : " . $id, self::STATUS_CODE_400);
            }
        } else {
            //自动生成文件标识
            $id = md5(($path !== '' ? '/' : '') . $path . '/' . $filename); //使用路径md5方便小文件记录到数据库
            //$id = md5($filename . '-' . (isset($metadata['type']) ? $metadata['type'] : '') . '-' . $size . '-' . (isset($metadata['lastModified']) ? $metadata['lastModified'] : '0') . '-' . $path);

            //$id = md5($filename . '-' . (isset($metadata['type']) ? $metadata['type'] : '') . '-' . $size . '-' . (isset($metadata['lastModified']) ? $metadata['lastModified'] : '0') . '-' . (isset($metadata['path']) ? $metadata['path'] : uniqid(mt_rand() . php_uname(), true)));
        }

        //创建文件信息
        $info = new TusFileInfo($id);
        $info->path = $path;
        if ($isFinal) {
            $info->concat = TusFileInfo::CONCAT_FINAL;
            $info->partial = $partial;
        } elseif ($isPartial) {
            $info->concat = TusFileInfo::CONCAT_PARTIAL;
        }
        $info->size = $size;
        $info->sizeIsDeferred = $sizeIsDeferred;
        $info->metadata = $metadata;
        $info->checksum = $this->verifyChecksum();
        //验证文件是否存在
        $infoResult = $this->store->newInfo($info);
        if (!$infoResult) {
            $this->setResCode(self::STATUS_CODE_500);
            return;
        }
        if (!$info->isTemp) { //文件已经上传
            $this->setResHeader("Upload-Offset", $size);
            $this->setResHeader('Location', self::getSiteUrl() . rtrim(self::getUri(), '/') . '/' . $id);
            $this->res_data = $info->toArray(); //兼容原生上传方式
            $this->setResCode(self::STATUS_CODE_201);
            return;
        }

        if ($isFinal) { //合并分块文件
            $info->offset = $size;
            if (!$this->store->merge($info)) {
                $this->store->delete($info); //失败清除
                throw new Exception('Post -> FailMerge', self::STATUS_CODE_400);
            }
            $this->ifComplete($info);
        }
        if ($containsChunk) { //有数据上传
            //info信息锁定判断
            if (!$this->store->lock($id)) { //locked
                $this->setResCode(self::STATUS_CODE_423);
                return;
            }

            $this->writeChunk($info);
            $this->store->unlock($id);
        }
        $this->setExpires($info);

        $this->setResHeader('Location', self::getSiteUrl() . rtrim(self::getUri(), '/') . '/' . $id);
        $this->setResCode(self::STATUS_CODE_201);
        $this->res_data = $info->toArray();
    }

    /**
     * 上传文件
     * PATCH /files/{upload-key}
     * @return void
     * @throws Exception
     */
    private function handlePatch()
    {
        if ($this->getReqHeader('Content-Type') !== 'application/offset+octet-stream') {
            throw new Exception('Patch ErrInvalidContentType', self::STATUS_CODE_400);
        }
        $offset = (int)$this->getReqHeader('Upload-Offset');
        if ($offset < 0) {
            throw new Exception('Patch ErrInvalidOffset', self::STATUS_CODE_400);
        }

        $id = $this->getKey();
        if (!$id) {
            throw new Exception('Patch ErrInvalidKey', self::STATUS_CODE_400);
        }
        //info Locked
        if (!$this->store->lock($id)) {
            $this->setResCode(self::STATUS_CODE_423);
            return;
        }

        try {
            //获取 info
            $info = $this->store->getInfo($id);
            if (!$info) {
                throw new Exception('ErrInvalidInfo', self::STATUS_CODE_400);
            }
            //数据已经完成
            if (false === $info->isTemp) { //!$info->sizeIsDeferred && $info->offset == $info->size
                $this->setResHeader("Upload-Offset", $info->offset);
                throw new Exception('exists', self::STATUS_CODE_204);
            }

            if ($info->isFinal()) {
                throw new Exception('IsFinal', self::STATUS_CODE_403);
            }
            if ($offset != $info->offset) {
                throw new Exception('ErrMismatchOffset ' . $offset . '<>' . $info->offset, self::STATUS_CODE_409);
            }

            $length = (int)$this->getReqHeader("Upload-Length");
            if ($length) {
                if (!$info->sizeIsDeferred) { //不是延迟上报上传大小
                    throw new Exception('ErrInvalidUploadLength', self::STATUS_CODE_400);
                }
                if ($length < 0 || $info->offset > $length || ($this->maxUploadSize > 0 && $length > $this->maxUploadSize)) {
                    throw new Exception('ErrInvalidUploadLength', self::STATUS_CODE_400);
                }

                $info->sizeIsDeferred = false;
                $info->size = $length;
            }

            //校验
            $this->verifyChecksum(true);

            $this->writeChunk($info);
            $this->store->unlock($id);
            $this->setResCode(self::STATUS_CODE_204);
        } catch (Exception $e) {
            $this->store->unlock($id);
            if ($e->getCode() !== self::STATUS_CODE_204) {
                static::log('Patch -> ' . $e->getMessage());
                $this->res_msg = $e->getMessage();
            }
            $this->setResCode($e->getCode());
        }
    }

    /**
     * 删除
     * DELETE /files/{upload-key}
     * @return void
     */
    private function handleDelete()
    {
        $id = $this->getKey();

        $info = $this->store->getInfo($id);
        if (!$info) {
            $this->setResCode(self::STATUS_CODE_404);
            return;
        }

        if ($info->isTemp && !$this->store->lock($id)) {
            $this->setResCode(self::STATUS_CODE_423);
            return;
        }

        //清理文件
        $this->store->delete($info);
        $info->isTemp && $this->store->unlock($id);

        $this->setResCode(self::STATUS_CODE_204);
    }

    /**
     * 获取文件
     * GET /files/{upload-key}
     * @return void
     * @throws Exception
     */
    private function handleGet()
    {
        if (!empty($_GET['file'])) { //是文件路径 模拟info
            $info = $this->store->getFile2Info($this->getReqPath());
            if ($info === null) {
                throw new Exception('', self::STATUS_CODE_404);
            }
        } else {
            $id = $this->getKey();
            $info = $this->id2info($id, !empty($_GET['hash']));
            if (is_int($info)) {
                $this->setResCode($info);
                return;
            }
        }

        $filename = $info->metadata['name'];
        if (isset($info->metadata['type'])) {
            $contentType = $info->metadata['type'];
        } else {
            $contentType = self::minMimeType($filename);
        }
        $this->setResHeader('Accept-Ranges', 'bytes');
        $this->setResHeader('Content-Type', $contentType);
        $this->setResHeader('Content-Disposition', (isset(self::$mimeTypeInline[$contentType]) ? 'inline' : 'attachment') . ';filename="' . $filename . '"');
        $this->setResHeader('Last-Modified', gmdate('D, d M Y H:i:s', $info->mtime ?: time()) . ' GMT');

        $offset = 0;
        $readSize = $info->offset; //-1; //读取的大小
        $endOffset = $info->offset - 1;
        $range = $this->getReqHeader('Range'); //bytes=0-5  bytes=-1  bytes=500-
        if ($range) {
            $ranges = explode('-', substr($range, 6));
            $offset = (int)$ranges[0];
            if ($offset < 0 || $offset > $info->offset) {
                $this->setResCode(self::STATUS_CODE_416);
                return;
            }
            if ($ranges[1] !== '') {
                $endOffset = (int)$ranges[1];
                if ($endOffset < 0 || $endOffset > $info->offset) {
                    $this->setResCode(self::STATUS_CODE_416);
                    return;
                }
            }
            $readSize = $endOffset - $offset + 1;

            $this->setResHeader('Content-Range', 'bytes ' . $offset . '-' . $endOffset . '/' . $info->offset);
            $this->setResHeader('Content-Length', $readSize);
            $this->setResCode(self::STATUS_CODE_206);
        } else {
            $this->setResHeader('Content-Length', $readSize); //$info->offset
            $this->setResCode(self::STATUS_CODE_200);
        }

        //Etag Last-Modified=date(self::RFC_7231)
        $this->res_data = $readSize == 0 ? null : $this->store->output($info, $offset, $readSize);
    }

    /**
     * 返回上传读取资源地址
     * @return string
     */
    private function inStream()
    {
        return $this->customInStream === '' ? 'php://input' : $this->customInStream;
    }

    //设置过期时间
    private function setExpires($info)
    {
        if ($this->uploadExpires > 0 && $info->mtime) {
            // RFC_7231
            $this->setResHeader('Upload-Expires', gmdate('D, d M Y H:i:s', $info->mtime + $this->uploadExpires) . ' GMT');
        }
    }

    /**
     * 通过id获取info
     * @param $id
     * @param bool $hash
     * @return TusFileInfo
     * @throws Exception
     */
    private function id2info($id, bool $hash = false)
    {
        if ($hash) {
            $info = $this->store->getHash2Info($id);
        } else {
            $info = $this->store->getInfo($id);
        }
        if (!$info) {
            throw new Exception('', self::STATUS_CODE_404);
        }
        return $info;
    }

    /**
     * 校验验证
     * @param bool $chunk 校验上传数据块
     * @return string|null
     * @throws Exception
     */
    private function verifyChecksum(bool $chunk = false)
    {
        $checksumStr = $this->getReqHeader('Upload-Checksum'); //校验算法 base64(检验值)
        if (!$checksumStr) {
            if ($this->enableChecksum && false === $chunk) {
                $this->setResHeader(null);
                throw new Exception('Upload-Checksum is empty', self::STATUS_CODE_400);
            }
            return null;
        }
        //启用校验
        if (!strpos($checksumStr, ' ')) {
            $this->setResHeader(null);
            throw new Exception('Upload-Checksum is invalid', self::STATUS_CODE_400);
        }

        [$algorithm, $checksum] = explode(' ', $checksumStr);
        if (!$checksum || strpos($this->allowAlgos, $algorithm) === false) {
            $this->setResHeader(null);
            throw new Exception("Upload-Checksum is invalid: " . $checksumStr, self::STATUS_CODE_400);
        }

        if ($chunk) { //验证每次上传数据块 todo 驻入处理
            //if($info->isPartial()===false){
            if ($checksum !== base64_encode(hash_file($algorithm, $this->inStream(), true))) {
                $this->setResHeader(null);
                throw new Exception('上传数据块校验不匹配', self::STATUS_CODE_460);
            }
            //}
        }
        return $checksumStr;
    }

    /**
     * 写入文件
     * @param TusFileInfo $info
     * @return bool
     * @throws Exception
     */
    private function writeChunk(TusFileInfo $info): bool
    {
        $length = (int)$this->getReqHeader('Content-Length');
        $offset = $info->offset;
        if (!$info->sizeIsDeferred && $offset + $length > $info->size) {
            if ($this->ifComplete($info)) { //检测一次是否是已经完成的
                return true;
            }

            throw new Exception("内容长度超出大小", self::STATUS_CODE_413);
        }

        if ($info->isPartial()) {
            //修正 Partial模式的数据记录大小 tus-php client上传会有此情况
            if ($length > 0 && $length != $info->size && $this->getReqHeader('Expect') == '100-Continue') {
                $info->size = $length;
            }
        }

        //$maxSize = -1;
        if ($length > 0) {
            $maxSize = $length;
            $info->offset = $offset + $length;
            if (false === $this->store->updateTmpInfo($info)) {
                throw new Exception('info文件已丢失' . $info, self::STATUS_CODE_400);
            }
        } else {
            $maxSize = $info->size - $info->offset;
            // the body size.
            if ($info->sizeIsDeferred) { //未指定大小时
                if ($this->maxUploadSize > 0) {
                    // Ensure that the upload does not exceed the maximum upload size
                    $maxSize = $this->maxUploadSize - $offset;
                } else {
                    // If no upload limit is given, we allow arbitrary sizes
                    $maxSize = PHP_INT_MAX;
                }
            }
        }

        //static::log('ChunkWriteStart', "key", $info->id, "maxSize", $maxSize, "offset", $offset, $this->inStream());
        $bytesWritten = 0;
        if ($this->customInStream === '' && $this->writeInStream) {
            $bytesWritten = call_user_func($this->writeInStream, $info, $maxSize);
        } else {
            $fp = fopen($this->inStream(), 'rb');
            if ($fp) {
                $bytesWritten = $this->store->write($fp, $info, $maxSize);
                fclose($fp);
            }
        }
        //static::log("ChunkWriteComplete", "id", $info->id, "bytesWritten", $bytesWritten);

        $newOffset = $offset + $bytesWritten;
        if ($info->offset != $newOffset) {
            $info->offset = $newOffset;
            $info->mtime = time();
            if (!$this->store->updateTmpInfo($info)) {
                throw new Exception('info文件已丢失', self::STATUS_CODE_400);
            }
        }
        // Send new offset to client
        $this->setResHeader("Upload-Offset", $newOffset);
        $this->setExpires($info);
        return $this->ifComplete($info);
    }

    /**
     * @param TusFileInfo $info
     * @return bool
     * @throws Exception
     */
    private function ifComplete(TusFileInfo $info): bool
    {
        if (!$info->sizeIsDeferred && $info->offset == $info->size) {
            try {
                $this->store->complete($info);
            } catch (Exception $e) {
                throw new Exception($e->getMessage(), self::STATUS_CODE_400);
            }
            return true;
        }
        return false;
    }
}
